How Dynamic Configuration Works

Configuration
Advanced
Dynamic Configuration
Author

Dario Airoldi

Published

January 16, 2026

How Dynamic Configuration Works

Diginsight provides a powerful dynamic configuration system that allows configuration values to be loaded from multiple sources and overridden at runtime.

This article explains how Diginsight manages configuration loading from files, HTTP headers, and volatile settings, and how you can scope configurations to specific classes using class-aware notation.

Configuration Sources Overview

Diginsight supports three main configuration sources:

Source Scope Persistence Use Case
File Configuration Application-wide Persistent Default values, environment-specific settings
Dynamic Configuration Request scope Per-request Per-request overrides via HTTP headers
Volatile Configuration Application-wide Runtime (until restart) Hot-switch settings without redeploy
┌─────────────────────────────────────────────────────────────────┐
│                    Configuration Priority                       │
│    (later sources override earlier ones)                        │
│                                                                 │
│   1. appsettings.json ──────────────────────────────►           │
│   2. appsettings.{Environment}.json ─────────────────►          │
│   3. Volatile Configuration (runtime storage) ───────►          │
│   4. Dynamic Configuration (HTTP headers) ───────────► FINAL    │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

1. File-Based Configuration

File-based configuration is the foundation. Options are loaded from appsettings.json using the standard .NET configuration system:

{
  "Diginsight": {
    "Activities": {
      "LogBehavior": "Show",
      "ActivityLogLevel": "Debug",
      "LoggedActivityNames": {
        "SmartCache.SetValue": "Hide",
        "SmartCache.OnEvicted": "Hide",
        "ServiceBusReceiver.Complete": "Hide"
      }
    }
  }
}

Register in Program.cs or Startup.cs:

services.ConfigureClassAware<DiginsightActivitiesOptions>(
    configuration.GetSection("Diginsight:Activities")
);

2. Dynamic Configuration (HTTP Headers)

Dynamic configuration allows per-request overrides via the Dynamic-Configuration HTTP header.

Values last only for the duration of a single request/scope.

How It Works

  1. HTTP Request arrives with Dynamic-Configuration header
  2. DefaultDynamicConfigurationLoader extracts key-value pairs from the header
  3. DynamicallyConfigureOptions builds an in-memory IConfiguration from these pairs
  4. Configuration is bound to the options object via the Filler pattern

Header Format

GET /api/weather HTTP/1.1
Dynamic-Configuration: LogBehavior=Show MaxAge=0 DisablePayloadRendering=true

Multiple values are space-separated: Key1=Value1 Key2=Value2 Key3=Value3

Registering Dynamic Configuration

To enable dynamic configuration for an options class:

services.ConfigureClassAware<DiginsightActivitiesOptions>(
        configuration.GetSection("Diginsight:Activities"))
    .DynamicallyConfigureClassAware<DiginsightActivitiesOptions>();

Configuration Flow

HTTP Header: "Dynamic-Configuration: LogBehavior=Show MaxAge=0"
                       │
                       ▼
┌──────────────────────────────────────────────────────────┐
│         DefaultDynamicConfigurationLoader.Load()         │
│  1. Get header value from HttpContext                    │
│  2. Parse w. DynamicHttpHeadersParser.ParseConfiguration │
│  3. Return KeyValuePair<string, string?>[]               │
└──────────────────────┬───────────────────────────────────┘
                       │
                       ▼
┌──────────────────────────────────────────────────────────┐
│         DynamicallyConfigureOptions.ConfigureCore()      │
│  1. Build IConfiguration from key-value pairs            │
│  2. Wrap with FilteredConfiguration (for class-aware)    │
│  3. configuration.Bind(options.MakeFiller())             │
└──────────────────────────────────────────────────────────┘

3. Volatile Configuration

Volatile configuration provides runtime-persistent overrides that survive across requests but are lost on application restart. Unlike dynamic configuration (per-request), volatile settings are stored in memory and apply to all requests.

Use Cases

  • Hot-switching feature flags without redeployment
  • Temporarily changing log levels for debugging
  • A/B testing different configurations

How It Works

Volatile configuration uses an IVolatileConfigurationStorage that holds configuration values in memory. When options are resolved, the volatile configuration is checked and applied.

Registering Volatile Configuration

services.ConfigureClassAware<MyOptions>(configuration.GetSection("MyOptions"))
    .VolatilelyConfigureClassAware<MyOptions>();

4. Class-Aware Configuration

Class-aware configuration allows you to scope settings to specific classes, enabling component-level or class-level configuration overrides.

The Class Marker Notation

In configuration files, use the @ symbol followed by the class name to scope settings:

{
  "Diginsight": {
    "Activities": {
      "LogBehavior": "Hide",
      "LogBehavior@MyNamespace.ImportantService": "Show",
      "LogBehavior@MyNamespace.VerboseService": "Truncate"
    }
  }
}

This means: - Default LogBehavior is Hide - For class MyNamespace.ImportantService, LogBehavior is Show - For class MyNamespace.VerboseService, LogBehavior is Truncate

HTTP Header Class-Aware Syntax

The same notation works in HTTP headers:

Dynamic-Configuration: LogBehavior=Hide LogBehavior@MyService=Show

How FilteredConfiguration Works

The FilteredConfiguration class wraps an IConfiguration and filters sections based on the class context:

public class FilteredConfiguration : IFilteredConfiguration
{
    public const char ClassDelimiter = '@';
    
    // When accessing "LogBehavior", FilteredConfiguration:
    // 1. Looks for "LogBehavior@FullClassName" (exact match)
    // 2. Falls back to "LogBehavior@ClassName" (short name)
    // 3. Falls back to "LogBehavior" (no class marker)
}

The class marker matching uses a priority system - more specific markers take precedence.

Using Class-Aware Options

To consume class-aware options, inject IClassAwareOptionsMonitor<T>:

public class MyService
{
    private readonly IClassAwareOptionsMonitor<DiginsightActivitiesOptions> optionsMonitor;

    public MyService(IClassAwareOptionsMonitor<DiginsightActivitiesOptions> optionsMonitor)
    {
        this.optionsMonitor = optionsMonitor;
    }

    public void DoWork()
    {
        // Get options scoped to THIS class
        var options = optionsMonitor.Get(GetType());
        
        // LogBehavior will be resolved based on class-aware markers
        if (options.LogBehavior == LogBehavior.Show)
        {
            // ...
        }
    }
}

5. The Filler Pattern: Partial Dynamic Configuration

The Filler pattern is a powerful mechanism that allows you to control which properties of an options class can be dynamically configured, and how complex types are serialized/deserialized for configuration binding.

Why Use a Filler?

When configuration is bound from HTTP headers or volatile storage, .NET’s IConfiguration.Bind() is used. However:

  1. Not all properties should be dynamically configurable - some are sensitive or should only be set at startup
  2. Complex types need conversion - dictionaries, collections, and custom types need string serialization/deserialization
  3. Different property names - the configuration key name might differ from the property name

The IDynamicallyConfigurable Interface

public interface IDynamicallyConfigurable
{
    /// <summary>
    /// Returns an object that "masks" the properties available for dynamic configuration.
    /// </summary>
    object MakeFiller();
}

The MakeFiller() method returns either: - this (default): All public properties are dynamically configurable - A custom Filler class: Only properties defined on the Filler are configurable

Creating a Custom Filler

Here’s an example showing the Filler pattern in action:

public class MyOptions : IDynamicallyConfigurable
{
    // These properties CAN be configured from files
    public string? Foo { get; set; }
    public double Baz { get; set; }
    public ICollection<string> Bars { get; private set; } = new List<string>();
    
    // This property should NOT be dynamically configurable
    public string? SensitiveValue { get; set; }

    // Return the Filler to control dynamic configuration
    object IDynamicallyConfigurable.MakeFiller() => new Filler(this);

    private class Filler
    {
        private readonly MyOptions filled;

        public Filler(MyOptions filled) => this.filled = filled;

        // Foo is exposed unchanged
        public string? Foo
        {
            get => filled.Foo;
            set => filled.Foo = value;
        }

        // Bars collection is exposed as a semicolon-separated string
        public string Bar
        {
            get => string.Join(";", filled.Bars);
            set => filled.Bars = value.Split(';').ToList();
        }

        // Baz is NOT exposed - cannot be dynamically configured
        // SensitiveValue is NOT exposed - cannot be dynamically configured
    }
}

What the Filler Controls

Aspect Description
Property Visibility Only properties defined on Filler can be dynamically configured
Property Name Filler property name is the configuration key name
Type Conversion Filler handles serialization (getter) and deserialization (setter)
Validation Filler setters can validate and sanitize input

Real-World Example: DiginsightActivitiesOptions

The DiginsightActivitiesOptions class demonstrates the Filler pattern for dictionary properties:

public sealed class DiginsightActivitiesOptions : IDynamicallyConfigurable, IVolatilelyConfigurable
{
    // Dictionary property - configured from JSON as nested object
    public IDictionary<string, LogBehavior> LoggedActivityNames { get; }
    
    // Simple properties
    public LogBehavior LogBehavior { get; set; }
    public LogLevel ActivityLogLevel { get; set; }
    public bool DisablePayloadRendering { get; set; }
    
    object IDynamicallyConfigurable.MakeFiller() => new Filler(this);
    object IVolatilelyConfigurable.MakeFiller() => new Filler(this);

    private class Filler
    {
        private readonly DiginsightActivitiesOptions filled;

        public Filler(DiginsightActivitiesOptions filled) => this.filled = filled;

        // Simple properties pass through unchanged
        public LogBehavior LogBehavior
        {
            get => filled.LogBehavior;
            set => filled.LogBehavior = value;
        }

        public LogLevel ActivityLogLevel
        {
            get => filled.ActivityLogLevel;
            set => filled.ActivityLogLevel = value;
        }

        public bool DisablePayloadRendering
        {
            get => filled.DisablePayloadRendering;
            set => filled.DisablePayloadRendering = value;
        }

        // Dictionary is exposed as space-separated key=value pairs
        public string LoggedActivityNames
        {
            get => string.Join(" ", filled.LoggedActivityNames.Select(kv => $"{kv.Key}={kv.Value}"));
            set
            {
                // Skip if unchanged (prevents unnecessary clear/repopulate)
                if (value == string.Join(" ", filled.LoggedActivityNames.Select(kv => $"{kv.Key}={kv.Value}")))
                    return;

                filled.LoggedActivityNames.Clear();
                filled.LoggedActivityNames.AddRange(
                    value.Split(' ', StringSplitOptions.RemoveEmptyEntries)
                        .Select(x => x.Split('=', 2) switch
                        {
                            [var k] => KeyValuePair.Create(k, LogBehavior.Show),
                            [var k, var v] when Enum.TryParse(v, true, out LogBehavior b) => KeyValuePair.Create(k, b),
                            _ => (KeyValuePair<string, LogBehavior>?)null,
                        })
                        .OfType<KeyValuePair<string, LogBehavior>>()
                );
            }
        }
    }
}

HTTP Header Format for Dictionary Properties

With the Filler above, you can override LoggedActivityNames via HTTP header:

Dynamic-Configuration: LoggedActivityNames=SmartCache.SetValue=Hide%20SmartCache.OnEvicted=Show

Note: Space is URL-encoded as %20 since space separates top-level entries.


6. Configuration Binding Flow

The complete flow from configuration source to options instance:

┌─────────────────────────────────────────────────────────────────┐
│                    Options Resolution Flow                       │
└─────────────────────────────────────────────────────────────────┘

1. ClassAwareOptionsFactory.Create(name, @class) is called
                       │
                       ▼
2. Create TOptions instance via Activator.CreateInstance<TOptions>()
                       │
                       ▼
3. Run IConfigureOptions<TOptions> configurators
   └── Binds appsettings.json via ConfigurationBinder
                       │
                       ▼
4. Run IConfigureClassAwareOptions<TOptions> configurators
   └── Including DynamicallyConfigureClassAwareOptions:
       a. Load specs from HTTP header via IDynamicConfigurationLoader
       b. Build in-memory IConfiguration from specs
       c. Wrap with FilteredConfiguration.For(configuration, @class)
       d. configuration.Bind(options.MakeFiller())
                       │
                       ▼
5. Run IPostConfigureOptions<TOptions> post-configurators
                       │
                       ▼
6. Run IPostConfigureClassAwareOptions<TOptions> post-configurators
   └── Including VolatilelyConfigureClassAwareOptions:
       a. Get configuration from IVolatileConfigurationStorage
       b. Wrap with FilteredConfiguration.For(configuration, @class)
       c. configuration.Bind(options.MakeFiller())
                       │
                       ▼
7. Run validators
                       │
                       ▼
8. Return configured TOptions instance

7. Best Practices

Filler Design Guidelines

  1. Only expose safe properties - Don’t expose sensitive configuration to dynamic override
  2. Use proper serialization - Always explicitly format complex types (don’t rely on ToString())
  3. Add setter guards - Check if value is unchanged before clearing/repopulating collections
  4. Validate input - Setters can reject invalid values

Configuration Naming

  1. Use consistent naming - Filler property names become configuration keys
  2. Document expected format - For complex types, document the string format expected

Class-Aware Configuration

  1. Use full class names - @MyNamespace.MyClass is more specific than @MyClass
  2. Order by specificity - More specific markers override less specific ones
  3. Test class resolution - Verify the correct configuration is resolved for each class

8. Summary

Concept Purpose
File Configuration Base configuration from appsettings.json
Dynamic Configuration Per-request overrides via HTTP headers
Volatile Configuration Runtime-persistent overrides
Class-Aware Scope configuration to specific classes using @ notation
Filler Pattern Control which properties are dynamically configurable and how they’re serialized
FilteredConfiguration Wraps IConfiguration to apply class-based filtering

The combination of these features provides a flexible, powerful configuration system that supports: - Environment-specific defaults - Per-request customization for debugging - Hot-switching features without redeploy - Component-level configuration granularity - Safe exposure of only appropriate properties for dynamic override

Back to top